package emulator.hardware.video;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import emulator.EmulatorException;
import emulator.GUI.display.VicDisplay;
import emulator.GUI.display.VicDisplayRegistry;
import emulator.hardware.bus.Bus;
import emulator.hardware.bus.VideoBus;
import emulator.hardware.clock.Clock;
import emulator.hardware.clock.ClockHandle;
import emulator.hardware.debug.BusWatchException;
import emulator.hardware.memory.UnmappedMemoryException;

public class Vic6561 extends Thread implements Bus
{
	static Logger logger = LogManager.getLogger(Vic6561.class.getName());
	private final static int full_screen_width = 284;  /* PAL width */
	private final static int full_screen_height = 312; /* PAL height */
	final static int horizontal_blank = 32;
	final static int vertical_blank = 40;

	private int [] registers = new int [16];
	private Bus bus = null;
	private ClockHandle clock = null;
	private Vic6561ParameterCache parameter_cache = new Vic6561ParameterCache();
	private int[] multicolor_map = new int[4];
	
	public Vic6561()
	{
		super();
		int [] defaults = {  12,  38, 150, 174,
							  0, 240,   0,   1,
							255, 255,   0,   0,
							  0,   0,   0,  27 };
		for (int i = 0; i < 16; i++)
			registers[i] = defaults[i];
		updateRegisterCache();
	}
	
	public void attach(Bus bus, Clock clock)
	{
		this.bus = bus;
		if (clock != null)
			this.clock = clock.acquireHandle();
	}
	
	public void detach()
	{
		this.bus = null;
		this.clock = null;
	}

	@Override
	public int read(int address) throws BusWatchException,
			UnmappedMemoryException {
		return registers[address & 0x0f];
	}

	@Override
	public void write(int address, int data) throws BusWatchException,
			UnmappedMemoryException {
		registers[address & 0x0f] = data & 0xFF;
		updateRegisterCache();
	}

	/* (non-Javadoc)
	 * @see java.lang.Thread#run()
	 */
	@Override
	public void run() {
		VicDisplay display = VicDisplayRegistry.waitDisplay("VicScreen");
		for (;;)
		{
			try {
				refreshDisplay(display);
				display.showScreen();
			} catch (EmulatorException ex) {
				logger.error("Emulator exception",ex);
			}
		}
	}

	public void refreshDisplay(VicDisplay display) throws EmulatorException {
		byte[] memory = ((VideoBus)bus).getMemory();
		int scan_line;
		/* Vertical blank */
		for (scan_line = 0; scan_line < vertical_blank; scan_line++)
		{
			setRasterCount(scan_line);
			clock.tick(full_screen_width/4);
		}
		/* Visible screen */
		for (int i = 0; i < VicDisplay.MAX_SCREEN_HEIGHT; i++) {
			setRasterCount(scan_line++);
			drawRasterLineDirect(display, memory, i);
		}
		/* Vertical blank */
		for (; scan_line < full_screen_height; scan_line++)
		{
			setRasterCount(scan_line);
			clock.tick(full_screen_width/4);
		}
	}

	private void drawRasterLineDirect(VicDisplay display, byte[] memory, int line) throws EmulatorException
	{
		// horizontal blank
		clock.tick(horizontal_blank/4);
		// line start
		int last_line = getLastLine();
		if (line >= parameter_cache.getFirstLine() && line < last_line)
		{
			int start_col = 0;
			int pix = 0;
			int frame_width = parameter_cache.getLineStart();
			if (frame_width > 0)
			{
				int frame_left = frame_width & 0xfffc;
				int frame_rem = frame_width & 3;
				for (; pix < frame_left; pix+=4)
				{
					clock.tick();
					display.putPixel(pix, line, parameter_cache.getFrameColour(), 4);
				}
				if (frame_rem > 0)
				{
					clock.tick();
					display.putPixel(pix, line, parameter_cache.getFrameColour(), frame_rem);
					pix += frame_rem;
				}
			}
			else
				start_col = -frame_width/8;
			int next_tick_trigger = (4 - (pix & 3)) & 3;
			for (int col = start_col; col < parameter_cache.getColumns() && pix < VicDisplay.MAX_SCREEN_WIDTH; col++)
			{
				clock.tick(2);
				int line_offset = line - parameter_cache.getFirstLine();
				if (line_offset < 0)
				{
					display.putPixel(pix, line, parameter_cache.getFrameColour(), 8);
					pix += 8;
					continue;
				}
				int row = line_offset / parameter_cache.getCharHeight();
				int ch_row = line_offset % parameter_cache.getCharHeight();
				int offset = row * parameter_cache.getColumns() + col;
				int code = memory[offset+parameter_cache.getVideoRamAddress()] & 0xff;
				int color = memory[offset+parameter_cache.getColourRamAddress()] & 0xff;
				int ch_base = parameter_cache.getCharRomAddress() + code * parameter_cache.getCharHeight();
				int ch_addr = ch_base+ch_row;
				if (ch_addr >= 0x2000 && ch_addr < 0x8000)
					ch_addr += 0x6000;
				int value = memory[ch_addr] & 0xff;
				if ((color & 0x08) == 0)
				{ // Hi-Res
					int mask = 0x80;
					for (int p = Math.min(7, VicDisplay.MAX_SCREEN_WIDTH-pix); p >= 0; --p)
					{
						boolean pixel_set = (value & mask) != 0;
						display.putPixel(pix++, line,
								pixel_set == parameter_cache.getColourMode() ? color&0x07 : parameter_cache.getBackgroundColour());
						mask >>= 1;
					}
				}
				else
				{ // Multi-Color
					int mask = 0xC0;
					multicolor_map[2] = color & 0x07;
					for (int bit_pos = 6; bit_pos >= 0 && pix < VicDisplay.MAX_SCREEN_WIDTH; bit_pos -= 2)
					{
						int color_index = (int) (value & mask) >> bit_pos;
						display.putPixel(pix, line, multicolor_map[color_index], 2);
						pix += 2;
						mask >>= 2;
					}
				}
			}
			if (next_tick_trigger > 0)
			{
				display.putPixel(pix, line, parameter_cache.getFrameColour(), next_tick_trigger);
				pix += next_tick_trigger;
			}
			for (; pix < VicDisplay.MAX_SCREEN_WIDTH; pix+=4)
			{
				clock.tick();
				display.putPixel(pix, line, parameter_cache.getFrameColour(), 4);
			}
		}
		else
		{
			for (int pix = 0; pix < VicDisplay.MAX_SCREEN_WIDTH; pix+=4)
			{
				clock.tick();
				display.putPixel(pix, line, parameter_cache.getFrameColour(), 4);
			}
		}
		// horizontal blank
		clock.tick((full_screen_width-VicDisplay.MAX_SCREEN_WIDTH-horizontal_blank)/4);
	}

	private int getLastLine() {
		return parameter_cache.getFirstLine() + parameter_cache.getRows()*parameter_cache.getCharHeight();
	}
	
	private void setRasterCount(int line) {
		if ((line & 1) != 0)
			registers[3] |= 0x80;
		else
			registers[3] &= 0x7f;
		registers[4] = (line>>1) & 0xff;
	}
	
	private void updateRegisterCache() {
		parameter_cache.update(registers);
		multicolor_map[0] = parameter_cache.getBackgroundColour();
		multicolor_map[1] = parameter_cache.getFrameColour();
		multicolor_map[3] = parameter_cache.getFourthColour();
	}
	
	public Vic6561ParameterCache getParameterObject()
	{
		return parameter_cache;
	}
}
